/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package avl.sv.client;

import avl.sv.client.tools.PanTool;
import avl.sv.shared.Rect;
import avl.sv.shared.sources.SlideSource;
import avl.tiff.TiffDirectoryBuffer;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JFrame;
import javax.swing.JSlider;
import javax.swing.KeyStroke;
import javax.swing.Timer;

/**
 *
 * @author benbryan
 */
public class SlideViewer extends javax.swing.JPanel implements MouseMotionListener, MouseListener, MouseWheelListener {

    MouseActionLogger mouseAction = new MouseActionLogger();
    private double imageOffsetX = 0, imageOffsetY = 0;
    private double magnification = 1;
    private double objectiveLensesMagnification = 100;//20;
    private Thumbnail thumbnailPannel;
    private final SlideSource slideSource;
    private JSlider zoomSlider;
    private ScheduledExecutorService worker = Executors.newSingleThreadScheduledExecutor();
    ScheduledFuture<?> hideZoomSliderTaskFuture;
    private ArrayList<SlideViewerPlugin> plugins = new ArrayList<SlideViewerPlugin>();
    SlideViewerToolPanel toolPanel;
    private final ROI_Manager roiManager;
    
    public SlideViewer(SlideSource slideSource) {
        this.slideSource = slideSource;
        slideSource.setPostBufferFillEvent(new Repaint());
        initComponents();
       
        thumbnailPannel = new Thumbnail(this);
        add(thumbnailPannel);
        thumbnailPannel.setSize(10, 10);
        thumbnailPannel.setLocation(0,0);
        
        toolPanel = new SlideViewerToolPanel(this);
        add(toolPanel);
        toolPanel.setVisible(true);
        
        roiManager = new ROI_Manager(this);
        add(roiManager);
        addComponentListener(new ComponentListener() {
            @Override
            public void componentResized(ComponentEvent evt) {
                SlideViewer sv =  (SlideViewer) evt.getSource();
                roiManager.setSize(roiManager.getPreferredSize().width, getHeight());
            }
            @Override
            public void componentMoved(ComponentEvent e) {            }

            @Override
            public void componentShown(ComponentEvent e) {            }

            @Override
            public void componentHidden(ComponentEvent e) {            }
        });

        setupKeyStrokes();
        setupZoomSlider();
        
        setVisible(true);
        addMouseMotionListener(this);
        addMouseListener(this);
        addMouseWheelListener(this);
        
        addMouseMotionListener(AdvancedVirtualMicroscope.getSolutionManager());
        addMouseListener(AdvancedVirtualMicroscope.getSolutionManager());
        
        setFocusable(true);
        
    }

    public SlideViewerToolPanel getToolPanel() {
        return toolPanel;
    }
        
    Runnable hideZoomSliderTask = new Runnable() {
        @Override
        public void run() {
            zoomSlider.setVisible(false);
        }
    };
    
    public void setImageRegion(Rectangle bounds){
        bounds.grow(bounds.width/4, bounds.height/4);
        double mW = (double) getWidth() / bounds.width;
        double mH = (double) getHeight() / bounds.height;
        double mag = Math.min(mW, mH);
        magnification = mag;
        setImageOffsetX(-bounds.x+(getWidth()/mag-bounds.width)/2);
        setImageOffsetY(-bounds.y+(getHeight()/mag-bounds.height)/2);
        repaint();
    }
    
    public void setImageOffsetX(double imageOffsetX) {
        this.imageOffsetX = imageOffsetX;
    }

    public void setImageOffsetY(double imageOffsetY) {
        this.imageOffsetY = imageOffsetY;
    }
            
    public void close(){
        roiManager.promptToSaveROIs();
        slideSource.clearBuffers();
        for (SlideViewerPlugin plugin:plugins){
            if (plugin instanceof JFrame){
                JFrame frame = (JFrame) plugin;
                frame.dispose();
            }
        }
    }
    
    public ROI_Manager getRoiManager() {
        return roiManager;
    }
    
    public SlideSource getSlideSource() {
        return slideSource;
    }
       
    private void setupKeyStrokes() {
        
        final Action zoomOut = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                double delta = 1-0.3;
                Point p = new Point(getWidth()/2, getHeight()/2);
                zoomInOnPoint(p, delta);
            }
        };
        getInputMap().put(KeyStroke.getKeyStroke('-'), "zoomOut");
        getInputMap().put(KeyStroke.getKeyStroke('_'), "zoomOut");
        getActionMap().put("zoomOut", zoomOut);    
        
        final Action zoomIn = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                double delta = 1+0.3;
                Point p = new Point(getWidth()/2, getHeight()/2);
                zoomInOnPoint(p, delta);
            }
        };
        getInputMap().put(KeyStroke.getKeyStroke('='), "zoomIn");
        getInputMap().put(KeyStroke.getKeyStroke('+'), "zoomIn");
        getActionMap().put("zoomIn", zoomIn);
        
        
        final int panStep = 20;
        final Action panLeft = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                imageOffsetX += panStep / magnification;
                repaint();
            }
        };
        getInputMap().put(KeyStroke.getKeyStroke("LEFT"), "panLeft");
        getActionMap().put("panLeft", panLeft);

        final Action panRight = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                imageOffsetX -= panStep / magnification;
                repaint();
            }
        };
        getInputMap().put(KeyStroke.getKeyStroke("RIGHT"), "panRight");
        getActionMap().put("panRight", panRight);
        
        final Action panUp = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                imageOffsetY += panStep / magnification;
                repaint();
            }
        };
        getInputMap().put(KeyStroke.getKeyStroke("UP"), "panUp");
        getActionMap().put("panUp", panUp);
        
        final Action panDown = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                imageOffsetY -= panStep / magnification;
                repaint();
            }
        };
        getInputMap().put(KeyStroke.getKeyStroke("DOWN"), "panDown");
        getActionMap().put("panDown", panDown);        
    }
    
    private void setupZoomSlider(){
        zoomSlider = new JSlider(JSlider.VERTICAL);
        zoomSlider.setMinimum((int)(0));
        zoomSlider.setMaximum((int)(objectiveLensesMagnification));
        zoomSlider.setSize(50, 100);
        zoomSlider.setVisible(false);
        double numOfTicks = 5;
        zoomSlider.setMajorTickSpacing((int)(objectiveLensesMagnification/numOfTicks));
        zoomSlider.setMinorTickSpacing((int)(objectiveLensesMagnification/numOfTicks));
        zoomSlider.setPaintTicks(true);
        zoomSlider.setPaintLabels(true);     
        add(zoomSlider);     
        zoomSlider.addMouseMotionListener(new MouseMotionListener() {
            @Override
            public void mouseMoved(MouseEvent e) {
                zoomSlider.setVisible(false);
            }

            @Override
            public void mouseDragged(MouseEvent e) {
            }
        });
    }

    public Point displaySpaceToImageSpace(Point p1) {
        Point2D p2 = new Point2D.Double(p1.x, p1.y);
        p2 = displaySpaceToImageSpace(p2);
        return new Point((int)p2.getX(), (int)p2.getY());
    }
    
    public Point2D displaySpaceToImageSpace(Point2D p1) {
        Point2D p2 = null;
        try {
            p2 = (Point2D) p1.clone();
            Graphics2D g = (Graphics2D) getGraphics().create();
            concatenateImageToDisplayTransform(g);
            g.getTransform().inverseTransform(p1, p2);
        } catch (NoninvertibleTransformException ex) {
        }
        return p2;
    }
        
    public void centerImage(){
        magnification = getDefaultMagnification();
        imageOffsetX = 0;
        imageOffsetY = 0;
    }

    public double getDefaultMagnification() {
        double mW = (double) getWidth() / slideSource.getImageDimX();
        double mH = (double) getHeight() / slideSource.getImageDimY();
        double mag = Math.min(mW, mH);
        return mag; 
    }
       
    public double getMagnification() {
        return magnification;
    }
    
    @Override
    protected void paintComponent(Graphics g) {
        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
        super.paintComponent(g);
        thumbnailPannel.adjustPosition();
        if (toolPanel != null){
            toolPanel.setLocation(getWidth()/2-toolPanel.getWidth()/2, 0);
        }
        g.setClip(0, 0, getWidth(), getHeight());
        if (slideSource != null) {
            paintDirsInOrder(g);
        }
        roiManager.paintROIs(g);
        
        SolutionManager sm = AdvancedVirtualMicroscope.getSolutionManager();
        if (sm != null){
            sm.paintROIs(this, g);
        }
        if (plugins != null){
            for (SlideViewerPlugin result:plugins){
                result.paintPlugin(g);
            }
        }
        getToolPanel().paintTools(g);
        paintROIManagerIndicator(g);
        Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
    }
    
    private void paintROIManagerIndicator(Graphics g){
        Graphics2D g2d = (Graphics2D) g.create();
        g2d.setColor(Color.BLACK);
        g2d.setStroke(new BasicStroke(3));
        g2d.setColor(Color.WHITE);
        g2d.fillRect(0,0,20,getHeight());
        g2d.setColor(Color.BLACK);
        g2d.drawRect(0,0,20,getHeight());
        AffineTransform at = new AffineTransform();
        at.concatenate(((Graphics2D)g).getTransform());
        at.translate(15, getHeight()/2);
        at.rotate(-Math.PI/2);
        g2d.setTransform(at);
        g2d.drawString("ROI Manager", 0, 0);        
    }
    
    private void paintDirsInOrder(Graphics g){
        TreeMap<Double, TiffDirectoryBuffer> treeMap = slideSource.getZoomMap();
        Entry<Double, TiffDirectoryBuffer> thumbnail = treeMap.firstEntry();
        paintDir(thumbnail.getValue(),g);
        
        SortedMap<Double, TiffDirectoryBuffer> m = treeMap.tailMap(thumbnail.getKey());
        Iterator<TiffDirectoryBuffer> dirIt = m.values().iterator();
        boolean oneMore = true;
        while(dirIt.hasNext()){
            TiffDirectoryBuffer dir = dirIt.next();
            if (dir.getZoomLevel() <= 4*magnification){
                paintDir(dir,g);
            } if (oneMore){
                paintDir(dir,g);
                oneMore = false;
            } else {
                dir.setTilesInView(null);
            }
        }
    }
    
    public void concatenateImageToDisplayTransform(Graphics2D g) {
        concatenateImageToDisplayTransform(g, 1.0);
    }
    public void concatenateImageToDisplayTransform(Graphics2D g, double zoomLevel) {
        g.scale(magnification, magnification);
        g.translate(imageOffsetX, imageOffsetY);
        g.scale(1/zoomLevel, 1/zoomLevel);
    }
       
    private void paintDir(TiffDirectoryBuffer dir, Graphics gOrig) {
        Graphics2D g = (Graphics2D)gOrig.create();

        concatenateImageToDisplayTransform(g, dir.getZoomLevel());
        Shape temp = g.getClip();
        if (temp == null){ //This may never happen
            return;
        }
        Rectangle2D clip = g.getClip().getBounds2D();      
        clip.setRect( clip.getMinX(), 
                      clip.getMinY(), 
                      Math.min(clip.getMaxX(), dir.getImageWidth())-clip.getMinX(), 
                      Math.min(clip.getMaxY(), dir.getImageLength())-clip.getMinY());
        g.setClip(clip);
        Rect imgInView = findImageInView(dir, g);
        Point tilesToPlot[] = slideSource.findTilesRect(dir, imgInView);
        slideSource.paintTiles(dir, g, tilesToPlot);
    }
            
    public Rect findImageInView(TiffDirectoryBuffer dir, Graphics2D g){
        Rectangle cb = g.getClipBounds();
        Rect out = new Rect( Math.max(0, cb.x), 
                             Math.max(0, cb.y), 
                             cb.x+cb.width, 
                             cb.y+cb.height);
        return out;
    }

    void addPlugin(final SlideViewerPlugin plugin) {
        if (plugins == null){
            plugins = new ArrayList<>();
        }
        plugin.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                try {
                    plugins.remove(plugin);
                    Object call = new Repaint().call();
                } catch (Exception ex) {}
                super.windowClosing(e);
            }
        });
        plugins.add(plugin);
        repaint();
    }

    void removePlugin(SlideViewerPlugin plugin) {
        plugins.remove(plugin);
    }
         
    private class Repaint implements Callable {
        @Override
        public Object call() throws Exception {
            repaint();
            return 0;
        }
    }

    @Override
    public void mouseDragged(MouseEvent e) {
        mouseAction.mouseDragged(e);
        if (mouseAction.getMouseButton3() == MouseActionLogger.ButtonStatus.PRESSED){
            imageOffsetX += (mouseAction.deltaX) / magnification;
            imageOffsetY += (mouseAction.deltaY) / magnification;
        }
        if (mouseAction.getMouseButton1() == MouseActionLogger.ButtonStatus.PRESSED){
            if (toolPanel.getSelectedTool() instanceof PanTool){
                imageOffsetX += (mouseAction.deltaX) / magnification;
                imageOffsetY += (mouseAction.deltaY) / magnification;
            }
        }
        
        repaint();
    }

    @Override
    public void mousePressed(MouseEvent e) {
        mouseAction.mousePressed(e);
    }

    @Override
    public void mouseReleased(MouseEvent e) {
        mouseAction.mouseReleased(e);
    }

    @Override
    public void mouseEntered(MouseEvent e){}

    @Override
    public void mouseExited(MouseEvent e){}
    
    private Timer roiManagerPopupTimer = new Timer(250, new AbstractAction() {
        @Override
        public void actionPerformed(ActionEvent e) {
            roiManager.setVisible(true);
            toolPanel.setVisible(false);
            roiManager.requestFocus();
        }
    });
    
    @Override
    public void mouseMoved(MouseEvent e) {   
        mouseAction.mouseMoved(e);
        Point p = e.getPoint();
        if (roiManager.isVisible()){
            if (p.x > roiManager.getWidth()){
                roiManagerPopupTimer.stop();
                roiManager.setVisible(false);
                toolPanel.setVisible(true);
            }
        } else {
            if (p.x < getWidth()/20){
                roiManagerPopupTimer.setRepeats(false);
                roiManagerPopupTimer.restart();
                roiManagerPopupTimer.start();
            } else {
                roiManagerPopupTimer.stop();
            }
        }
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        requestFocus();
    }

    @Override
    public void mouseWheelMoved(MouseWheelEvent e) {
        Point p = e.getPoint();
        double delta = 1 - e.getWheelRotation() * 0.2;
        zoomInOnPoint(p, delta);
    }
    
    public void zoomInOnPoint(Point p, double delta){
        if (p == null){
            return;
        }
        double ref[] = {p.getX(),   p.getY()};
        double marksPre[] = new double[ref.length];
        double marksPost[] = new double[ref.length];

        Graphics2D gTemp = (Graphics2D) getGraphics().create();
        try {
            gTemp.setTransform(new AffineTransform());
            concatenateImageToDisplayTransform(gTemp);
            gTemp.getTransform().inverseTransform(ref, 0, marksPre, 0, 1);
        } catch (NoninvertibleTransformException ex) {       }

        changeMagnification(delta);

        try {
            gTemp.setTransform(new AffineTransform());
            concatenateImageToDisplayTransform(gTemp);
            gTemp.getTransform().inverseTransform(ref, 0, marksPost, 0, 1);
        } catch (NoninvertibleTransformException ex) {       }
        
        imageOffsetX += marksPost[0]-marksPre[0];
        imageOffsetY += marksPost[1]-marksPre[1];    
    }

    public void changeMagnification(double delta) {
        if ((slideSource == null) || (delta == 0)) {
            return;
        }
        double minMagnification = getDefaultMagnification()/2;
        double maxMagnification = 1;
        double newMag = magnification * delta;
        if (newMag > maxMagnification) {
            magnification = maxMagnification;
        } if (newMag < minMagnification){
            magnification = minMagnification;
        } else {
            magnification *= delta;
        }
        
        zoomSlider.setValue((int)(magnification*objectiveLensesMagnification));
        Point p = mouseAction.getLastMouseLocation();
        Dimension s = zoomSlider.getSize();
        zoomSlider.setLocation(p.x-s.width,p.y-s.height/2);
        zoomSlider.setVisible(true);
        if (hideZoomSliderTaskFuture != null){
            hideZoomSliderTaskFuture.cancel(true);
        }
        hideZoomSliderTaskFuture = worker.schedule(hideZoomSliderTask, 1, TimeUnit.SECONDS);
        
        repaint();
    }
    
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        addMouseListener(new java.awt.event.MouseAdapter() {
            public void mouseExited(java.awt.event.MouseEvent evt) {
                formMouseExited(evt);
            }
        });

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
        this.setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 786, Short.MAX_VALUE)
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 504, Short.MAX_VALUE)
        );
    }// </editor-fold>//GEN-END:initComponents

    private void formMouseExited(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_formMouseExited
        roiManagerPopupTimer.stop();
    }//GEN-LAST:event_formMouseExited

    // Variables declaration - do not modify//GEN-BEGIN:variables
    // End of variables declaration//GEN-END:variables
}
